use std::path::Path;

use serde::{Deserialize, Serialize};

use bc_db::{BaseBcDb, DBError, DBIteratorMode};

pub(super) mod block;
pub(super) mod block_index;
// pub(super) mod misc;
pub(super) mod misc_item;
pub(super) mod transaction;
pub(super) mod transaction_index;

pub use std::collections::HashMap as DbHashMap;

#[derive(Debug)]
struct StorageBase<K, V> {
    base_db: BaseBcDb,
    _phantom_key: std::marker::PhantomData<K>,
    _phantom_value: std::marker::PhantomData<V>,
}

impl<K, V> StorageBase<K, V>
where
    K: for<'de> Deserialize<'de> + Serialize + Eq + std::hash::Hash + Send + Sync + 'static,
    V: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
{
    fn new(path: &Path) -> Result<Self, DBError> {
        Ok(Self {
            base_db: BaseBcDb::new(path)?,
            _phantom_key: std::marker::PhantomData,
            _phantom_value: std::marker::PhantomData,
        })
    }

    // insert or update
    async fn put(&self, key: &K, value: &V) -> Result<(), DBError> {
        let k = StorageBase::<K, V>::serialize_key(key);
        let v = StorageBase::<K, V>::serialize_value(value);
        self.base_db.put(&k, v).await?;
        Ok(())
    }

    async fn get(&self, key: &K) -> Result<Option<V>, DBError> {
        let k = StorageBase::<K, V>::serialize_key(key);
        let v = self.base_db.get(&k).await?;
        if let Some(v) = v {
            let value = StorageBase::<K, V>::deserialize_value(&v);
            Ok(Some(value))
        } else {
            Ok(None)
        }
    }

    async fn erase(&self, key: &K) -> Result<(), DBError> {
        let k = StorageBase::<K, V>::serialize_key(key);
        self.base_db.erase(&k).await?;
        Ok(())
    }

    async fn exists(&self, key: &K) -> bool {
        let k = StorageBase::<K, V>::serialize_key(key);
        self.base_db.exists(&k).await
    }

    async fn flush_database(&self) -> Result<(), DBError> {
        self.base_db.flush().await?;
        Ok(())
    }

    async fn get_all_data(&self) -> DbHashMap<K, V> {
        let db = self.base_db.db.clone();

        let val = tokio::task::spawn_blocking(move || {
            let mut map = DbHashMap::new();
            for result in db.iterator(DBIteratorMode::Start) {
                let (key, value) = result.unwrap();
                let key = StorageBase::<K, V>::deserialize_key(&key);
                let value = StorageBase::<K, V>::deserialize_value(&value);
                map.insert(key, value);
            }

            map
        })
        .await;

        match val {
            Ok(map) => map,
            Err(e) => {
                tracing::error!("get all data failed: {}", e);

                DbHashMap::new()
            }
        }
    }

    #[cfg(debug_assertions)]
    async fn get_item_number(&self) -> usize {
        self.base_db.get_item_number().await.unwrap()
    }

    fn serialize_key(key: &K) -> String {
        hex::encode(bincode::serialize(key).unwrap())
    }

    fn deserialize_key(key: &[u8]) -> K {
        bincode::deserialize(&hex::decode(key).unwrap()).unwrap()
    }

    fn serialize_value(value: &V) -> Vec<u8> {
        bincode::serialize(value).unwrap()
    }

    fn deserialize_value(value: &[u8]) -> V {
        bincode::deserialize(value).unwrap()
    }
}

// unit test in bc_lib/src/blockchain_storage/storage/block_index.rs

#[cfg(test)]
mod tests {
    use super::*;
    use scopeguard::defer;

    use bc_base::{TransactionId, test_utils::test_tx, transaction::Transaction};

    // new, put, flush, exist
    #[ignore = "todo: fix exists bug"]
    #[tokio::test]
    async fn test_put_flush_exist() {
        let tmp_dir = bc_util::convert_to_temp_dir("test_put_flush_exist");
        let path = tmp_dir.path().to_path_buf();
        defer! {
            tmp_dir.close().unwrap();
        };

        let tx_1 = test_tx::new_tx_simple(100);
        let tx_1_id = tx_1.calc_tx_id();
        let tx_2 = test_tx::new_tx_simple(101);
        let tx_2_id = tx_2.calc_tx_id();
        assert_ne!(&tx_1_id, &tx_2_id);

        let db = StorageBase::<TransactionId, Transaction>::new(&path).unwrap();

        assert!(db.put(&tx_1_id, &tx_1).await.is_ok());
        assert!(!db.exists(&tx_2_id).await);
        assert!(db.flush_database().await.is_ok());
        assert!(!db.exists(&tx_2_id).await);
    }
}
